package com.jiangyg.mall.search.service;

import com.jiangyg.mall.core.utils.JsonUtils;
import com.jiangyg.mall.core.utils.Logger;
import com.jiangyg.mall.search.dto.SkuIndexDTO;
import com.jiangyg.mall.search.dto.SkuSearchParamsDTO;
import com.jiangyg.mall.search.dto.SkuSuggestSearchParamsDTO;
import com.jiangyg.mall.search.vo.SkuDetailsVO;
import com.jiangyg.mall.search.vo.SkuSearchResultVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.apache.dubbo.config.annotation.Service;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.bulk.BulkItemResponse;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.Aggregations;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.filter.FilterAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.filter.ParsedFilter;
import org.elasticsearch.search.aggregations.bucket.nested.NestedAggregationBuilder;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.elasticsearch.search.suggest.Suggest;
import org.elasticsearch.search.suggest.SuggestBuilder;
import org.elasticsearch.search.suggest.SuggestBuilders;
import org.elasticsearch.search.suggest.completion.CompletionSuggestion;
import org.springframework.beans.factory.annotation.Qualifier;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

import static com.jiangyg.mall.search.constant.SkuConstant.*;

// TODO 以后 es 升级 8+，client 支持 lamdba 表达式，可读性更强
@Slf4j
@Service
public class SkuSearchServiceImpl implements SkuSearchService {

    @Resource
    @Qualifier("esRestClient")
    private RestHighLevelClient esRestClient;

    /**
     * 公共属性
     */
    private static final RequestOptions OPTIONS;

    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
        // builder.addHeader("Authorization", "Bearer " + TOKEN);
        // builder.setHttpAsyncResponseConsumerFactory(
        //         new HttpAsyncResponseConsumerFactory
        //                 .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
        OPTIONS = builder.build();
    }

    @Override
    public boolean save(List<SkuIndexDTO> skuList) throws IOException {
        if (CollectionUtils.isEmpty(skuList)) {
            return true;
        }
        // 构造批量索引请求
        BulkRequest bulkRequest = new BulkRequest();
        for (SkuIndexDTO sku : skuList) {
            IndexRequest indexRequest = new IndexRequest(SKUS_INDEX);
            indexRequest.id(String.valueOf(sku.getId()));
            indexRequest.source(JsonUtils.toJSONString(sku), XContentType.JSON);
            bulkRequest.add(indexRequest);
        }
        // 保存索引
        BulkResponse bulk = esRestClient.bulk(bulkRequest, OPTIONS);
        final List<String> result = Arrays.stream(bulk.getItems()).map(BulkItemResponse::getId).collect(Collectors.toList());
        Logger.info(log, () -> String.format("商品上架完成：%s", JsonUtils.toJSONString(result)));
        return !bulk.hasFailures();
    }

    @Override
    public SkuSearchResultVO select(SkuSearchParamsDTO dto) throws IOException {
        // TODO 追加同义词检索
        // TODO 追加拼音支持
        // TODO 拼写纠错
        // 构建查询请求
        final SearchRequest searchRequest = this.buildSearchRequest(dto);
        // 执行检索请求
        final SearchResponse response = esRestClient.search(searchRequest, OPTIONS);
        // 查询结果封装
        return this.transformSearchResult(response, dto);
    }

    /**
     * 功能描述：转换查询结果
     *
     * @param response 响应
     * @param dto      参数
     * @return 结果
     */
    private SkuSearchResultVO transformSearchResult(SearchResponse response, SkuSearchParamsDTO dto) {
        final SkuSearchResultVO result = new SkuSearchResultVO();
        // 1. 返回命中的商品信息
        result.setSkus(extractSkuInfo(response, dto));
        // 2. 当前商品涉及到的所有属性信息
        result.setAttrs(extractAttrInfo(response));
        // 3. 当前商品涉及到的所有品牌信息
        result.setBrands(extractBrandInfo(response));
        // 4. 当前商品涉及到的所有分类信息
        result.setCategorys(extractCategoryInfo(response));
        // 5. 分页信息
        extractPageInfo(result, response, dto);
        return result;
    }

    /**
     * 功能描述：抽取分页信息
     *
     * @param response 请求返回
     * @param dto      请求参数
     */
    private void extractPageInfo(SkuSearchResultVO result, SearchResponse response, SkuSearchParamsDTO dto) {
        // 当前页码
        result.setCurrent(dto.getCurrent());
        // 总条数
        long total = response.getHits().getTotalHits().value;
        result.setTotal(total);
        // 总页数计算
        int size = pageSize(dto);
        int totalPages = (int) total % size == 0 ? (int) total / size : ((int) total / size + 1);
        result.setPages(totalPages);
    }

    /**
     * 功能描述：抽取聚合的分类信息
     *
     * @param response 请求返回
     * @return 分类信息
     */
    private List<SkuSearchResultVO.Category> extractCategoryInfo(SearchResponse response) {
        // category_agg_nested
        final ParsedNested categoryAggNested = response.getAggregations().get(CATEGORY_AGG_NESTED);
        if (categoryAggNested == null) {
            return Collections.emptyList();
        }
        // category_agg
        final ParsedLongTerms categoryAgg = categoryAggNested.getAggregations().get(CATEGORY_AGG);
        if (categoryAgg == null) {
            return Collections.emptyList();
        }
        // 生成分类信息
        return categoryAgg.getBuckets().stream()
                .map(bucket -> {
                    final SkuSearchResultVO.Category category = new SkuSearchResultVO.Category();
                    // 得到分类的ID
                    category.setId(String.valueOf(bucket.getKeyAsNumber().longValue()));
                    final ParsedStringTerms nameAgg = bucket.getAggregations().get(NAME_AGG);
                    nameAgg.getBuckets().stream().findFirst().ifPresent(a -> {
                        // 得到分类的名字
                        category.setName(a.getKeyAsString());
                    });
                    return category;
                })
                .collect(Collectors.toList());
    }

    /**
     * 功能描述：抽取聚合的品牌信息
     *
     * @param response 请求返回
     * @return 品牌信息
     */
    private List<SkuSearchResultVO.Brand> extractBrandInfo(SearchResponse response) {
        // brand_agg_nested
        final ParsedNested brandAggNested = response.getAggregations().get(BRAND_AGG_NESTED);
        if (brandAggNested == null) {
            return Collections.emptyList();
        }
        // brand_agg
        final ParsedLongTerms brandAgg = brandAggNested.getAggregations().get(BRAND_AGG);
        if (brandAgg == null) {
            return Collections.emptyList();
        }
        // 生成品牌信息
        return brandAgg.getBuckets().stream()
                .map(bucket -> {
                    final SkuSearchResultVO.Brand brand = new SkuSearchResultVO.Brand();
                    // 得到品牌的ID
                    brand.setId(String.valueOf(bucket.getKeyAsNumber().longValue()));
                    // 得到品牌的名字
                    final ParsedStringTerms nameAgg = bucket.getAggregations().get(NAME_AGG);
                    nameAgg.getBuckets().stream().findFirst().ifPresent(a -> brand.setName(a.getKeyAsString()));
                    // 得到品牌的logo
                    final ParsedStringTerms logoAgg = bucket.getAggregations().get(LOGO_AGG);
                    logoAgg.getBuckets().stream().findFirst().ifPresent(a -> brand.setLogo(a.getKeyAsString()));
                    return brand;
                })
                .collect(Collectors.toList());
    }

    /**
     * 功能描述：抽取聚合的属性信息
     *
     * @param response 请求返回
     * @return 属性信息
     */
    private List<SkuSearchResultVO.Attr> extractAttrInfo(SearchResponse response) {
        // attr_agg_nested
        final ParsedNested attrAggNested = response.getAggregations().get(ATTR_AGG_NESTED);
        if (attrAggNested == null) {
            return Collections.emptyList();
        }
        // searchable-attr-filter
        final ParsedFilter searchableAttrFilter = attrAggNested.getAggregations().get(SEARCHABLE_ATTR_FILTER);
        if (searchableAttrFilter == null) {
            return Collections.emptyList();
        }
        // attr_agg
        final ParsedLongTerms attrAgg = searchableAttrFilter.getAggregations().get(ATTR_AGG);
        if (attrAgg == null) {
            return Collections.emptyList();
        }
        // 生成商品属性列表
        return attrAgg.getBuckets().stream()
                .map(bucket -> {
                    final SkuSearchResultVO.Attr attr = new SkuSearchResultVO.Attr();
                    // 得到属性的ID
                    attr.setId(String.valueOf(bucket.getKeyAsNumber().longValue()));
                    // bucket -> aggregations
                    final Aggregations aggregations = bucket.getAggregations();
                    // 得到属性的名字
                    final ParsedStringTerms nameAgg = aggregations.get(NAME_AGG);
                    if (CollectionUtils.isNotEmpty(nameAgg.getBuckets())) {
                        attr.setName(nameAgg.getBuckets().get(0).getKeyAsString());
                    }
                    // 得到属性的所有值
                    ParsedStringTerms attrValueAgg = aggregations.get(VALUE_AGG);
                    if (CollectionUtils.isNotEmpty(attrValueAgg.getBuckets())) {
                        final List<String> list = attrValueAgg.getBuckets().stream()
                                .map(MultiBucketsAggregation.Bucket::getKeyAsString)
                                .collect(Collectors.toList());
                        attr.setValues(list);
                    }
                    return attr;
                })
                .collect(Collectors.toList());
    }

    /**
     * 功能描述：抽取命中的商品信息
     *
     * @param response 请求返回
     * @param dto      请求参数
     * @return 商品信息
     */
    private List<SkuSearchResultVO.Sku> extractSkuInfo(SearchResponse response, SkuSearchParamsDTO dto) {
        return Arrays.stream(response.getHits().getHits())
                .map(hit -> {
                    // 获取命中数据、高亮字段的内容
                    final Map<String, Object> sourceAsMap = hit.getSourceAsMap();
                    final Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                    // 生成结果对象
                    final SkuSearchResultVO.Sku sku = new SkuSearchResultVO.Sku();
                    sku.setId(MapUtils.getString(sourceAsMap, ID));
                    sku.setTitle(getHighlightFieldValue(TITLE, highlightFields, sourceAsMap));
                    sku.setSubtitle(getHighlightFieldValue(SUBTITLE, highlightFields, sourceAsMap));
                    sku.setPrice(MapUtils.getDouble(sourceAsMap, PRICE));
                    // 生成图片集
                    if (sourceAsMap.containsKey(IMAGES)) {
                        final List<SkuSearchResultVO.Image> images =
                                ((List<Map<String, Object>>) sourceAsMap.get(IMAGES))
                                        .stream()
                                        .map(item -> {
                                            final String url = MapUtils.getString(item, URL);
                                            final boolean isDefault = MapUtils.getBoolean(item, DEFAULT);
                                            return new SkuSearchResultVO.Image(url, isDefault);
                                        })
                                        .collect(Collectors.toList());
                        sku.setImages(images);
                    }
                    return sku;
                })
                .collect(Collectors.toList());
    }

    /**
     * 功能描述：获取高亮字段
     *
     * @param name            字段名称
     * @param highlightFields 高亮字段
     * @param sourceAsMap     返回结果
     */
    private String getHighlightFieldValue(String name, Map<String, HighlightField> highlightFields, Map<String, Object> sourceAsMap) {
        if (highlightFields.containsKey(name)) {
            final HighlightField subtitle = highlightFields.get(name);
            final Text[] fragments = subtitle.getFragments();
            if (fragments != null && fragments.length > 0) {
                return fragments[0].string();
            }
        }
        return MapUtils.getString(sourceAsMap, name);
    }

    /**
     * 功能描述：构建查询请求
     *
     * @param dto 参数
     * @return 结果
     */
    private SearchRequest buildSearchRequest(SkuSearchParamsDTO dto) {
        final SearchSourceBuilder builder = new SearchSourceBuilder();
        // 1. 复合查询条件
        builder.query(assembleBoolQueryBuilder(dto));
        // 2. 排序
        assembleSort(builder, dto);
        // 3. 分页
        assemblePage(builder, dto);
        // 4. 标题和副标题高亮
        assembleHighlight(builder, dto);
        // 5. 按照品牌聚合
        assembleBrandAgg(builder);
        // 6. 按照分类信息进行聚合
        assembleCategoryAgg(builder);
        // 8. 按照属性信息进行聚合
        assembleAttrAgg(builder);
        // 9. 排除查询列
        builder.fetchSource(null, SELECT_EXCLUDE_FIELDS);
        // 构建查询请求
        Logger.info(log, () -> String.format("构建查询语句：%s", builder));
        return new SearchRequest(new String[]{SKUS_INDEX}, builder);
    }

    /**
     * 功能描述：构建品牌聚合
     *
     * @param builder 查询条件构建器
     */
    private void assembleAttrAgg(SearchSourceBuilder builder) {
        // 1. 根据属性ID进行聚合
        TermsAggregationBuilder attrAggBuilder = AggregationBuilders.terms(ATTR_AGG);
        attrAggBuilder.field(BASE_ATTRS_ID);
        // 1.1 在每个属性ID下，按照属性名进行聚合
        attrAggBuilder.subAggregation(AggregationBuilders.terms(NAME_AGG).field(BASE_ATTRS_NAME).size(1));
        // 1.2 在每个属性ID下，按照属性值进行聚合
        attrAggBuilder.subAggregation(AggregationBuilders.terms(VALUE_AGG).field(BASE_ATTRS_VALUE).size(50));
        // 2. 设置聚合过滤（只聚合可检索的属性）
        final FilterAggregationBuilder searchableAttrFilterAgg = AggregationBuilders.filter(SEARCHABLE_ATTR_FILTER,
                QueryBuilders.termQuery(BASE_ATTRS_SEARCHABLE, true));
        searchableAttrFilterAgg.subAggregation(attrAggBuilder);
        // 3. 嵌套聚合
        final NestedAggregationBuilder nestedAggBuilder = AggregationBuilders.nested(ATTR_AGG_NESTED, BASE_ATTRS);
        nestedAggBuilder.subAggregation(searchableAttrFilterAgg);
        builder.aggregation(nestedAggBuilder);
    }

    /**
     * 功能描述：构建品牌聚合
     *
     * @param builder 查询条件构建器
     */
    private void assembleCategoryAgg(SearchSourceBuilder builder) {
        // 分类聚合
        TermsAggregationBuilder categoryAggBuilder = AggregationBuilders.terms(CATEGORY_AGG);
        categoryAggBuilder.field(CATEGORY_ID).size(20);
        categoryAggBuilder.subAggregation(AggregationBuilders.terms(NAME_AGG).field(CATEGORY_NAME).size(1));
        // 嵌套聚合
        final NestedAggregationBuilder nestedAggBuilder = AggregationBuilders.nested(CATEGORY_AGG_NESTED, CATEGORY);
        nestedAggBuilder.subAggregation(categoryAggBuilder);
        builder.aggregation(nestedAggBuilder);
    }

    /**
     * 功能描述：构建品牌聚合
     *
     * @param builder 查询条件构建器
     */
    private void assembleBrandAgg(SearchSourceBuilder builder) {
        // 品牌聚合（根据品牌ID分组）
        final TermsAggregationBuilder brandAggBuilder = AggregationBuilders.terms(BRAND_AGG);
        brandAggBuilder.field(BRAND_ID).size(50);
        // 品牌的子聚合-品牌名聚合
        brandAggBuilder.subAggregation(AggregationBuilders.terms(NAME_AGG).field(BRAND_NAME).size(1));
        // 品牌的子聚合-品牌图片聚合
        brandAggBuilder.subAggregation(AggregationBuilders.terms(LOGO_AGG).field(BRAND_LOGO).size(1));
        // 嵌套聚合
        final NestedAggregationBuilder nestedAggBuilder = AggregationBuilders.nested(BRAND_AGG_NESTED, BRAND);
        nestedAggBuilder.subAggregation(brandAggBuilder);
        builder.aggregation(nestedAggBuilder);
    }

    /**
     * 功能描述：构建高亮
     *
     * @param builder 查询条件构建器
     * @param dto     请求参数
     */
    private void assembleHighlight(SearchSourceBuilder builder, SkuSearchParamsDTO dto) {
        if (StringUtils.isBlank(dto.getKeyword())) {
            return;
        }
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field(TITLE);
        highlightBuilder.field(SUBTITLE);
        highlightBuilder.preTags(HIGHLIGHT_PRE_TAG);
        highlightBuilder.postTags(HIGHLIGHT_POST_TAG);
        builder.highlighter(highlightBuilder);
    }

    /**
     * 功能描述：构建分页
     *
     * @param builder 查询条件构建器
     * @param dto     请求参数
     */
    private void assemblePage(SearchSourceBuilder builder, SkuSearchParamsDTO dto) {
        int size = pageSize(dto);
        builder.from((dto.getCurrent() - 1) * size);
        builder.size(size);
    }

    /**
     * 功能描述：商品每页条数
     *
     * @param dto 请求参数
     * @return 每页条数
     */
    private int pageSize(SkuSearchParamsDTO dto) {
        int size = dto.getSize();
        if (size > 100) {
            size = 100;
        }
        if (size < 10) {
            size = 10;
        }
        return size;
    }

    /**
     * 功能描述：构建排序
     *
     * @param builder 查询条件构建器
     * @param dto     请求参数
     */
    private void assembleSort(SearchSourceBuilder builder, SkuSearchParamsDTO dto) {
        final String sort = dto.getSort();
        if (StringUtils.isBlank(sort)) {
            return;
        }
        final String[] ss = sort.split("_");
        if (ss.length >= 2) {
            final SortOrder order = SortOrder.fromString(ss[1]);
            builder.sort(ss[0], order);
        }
    }

    /**
     * 功能描述：根据请求参数，构建复合查询条件
     *
     * @param dto 请求参数
     * @return 复合查询条件
     */
    private BoolQueryBuilder assembleBoolQueryBuilder(SkuSearchParamsDTO dto) {
        final BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        // 1 标题模糊查询
        if (StringUtils.isNotBlank(dto.getKeyword())) {
            final BoolQueryBuilder keywordBoolQueryBuilder = new BoolQueryBuilder();
            // 标题
            keywordBoolQueryBuilder.should(QueryBuilders.matchQuery(TITLE, dto.getKeyword()));
            // 副标题
            keywordBoolQueryBuilder.should(QueryBuilders.matchQuery(SUBTITLE, dto.getKeyword()));
            // 分类
            final MatchQueryBuilder categoryNameMatchQueryBuilder = QueryBuilders.matchQuery(CATEGORY_NAME, dto.getKeyword());
            final NestedQueryBuilder categoryNameNestedQueryBuilder = QueryBuilders.nestedQuery(CATEGORY, categoryNameMatchQueryBuilder, ScoreMode.None);
            keywordBoolQueryBuilder.should(categoryNameNestedQueryBuilder);
            // 品牌
            final MatchQueryBuilder brandNameMatchQueryBuilder = QueryBuilders.matchQuery(BRAND_NAME, dto.getKeyword());
            final NestedQueryBuilder brandNameNestedQueryBuilder = QueryBuilders.nestedQuery(BRAND, brandNameMatchQueryBuilder, ScoreMode.None);
            keywordBoolQueryBuilder.should(brandNameNestedQueryBuilder);
            // 与 filter 为 and 关系
            boolQueryBuilder.must(keywordBoolQueryBuilder);
        }
        // 2 三级分类查询
        if (StringUtils.isNotBlank(dto.getCate())) {
            final String[] cateIds = dto.getCate().split(",");
            final BoolQueryBuilder categoryBoolQueryBuilder = new BoolQueryBuilder();
            categoryBoolQueryBuilder.filter(QueryBuilders.termsQuery(CATEGORY_ID, cateIds));
            final NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery(CATEGORY, categoryBoolQueryBuilder, ScoreMode.None);
            boolQueryBuilder.filter(nestedQueryBuilder);
        }
        // 3 品牌查询
        if (StringUtils.isNotBlank(dto.getBrand())) {
            final String[] brandIds = dto.getBrand().split(",");
            final BoolQueryBuilder brandBoolQueryBuilder = new BoolQueryBuilder();
            brandBoolQueryBuilder.filter(QueryBuilders.termsQuery(BRAND_ID, brandIds));
            final NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery(BRAND, brandBoolQueryBuilder, ScoreMode.None);
            boolQueryBuilder.filter(nestedQueryBuilder);
        }
        // 4 是否有库存
        if (StringUtils.isNotBlank(dto.getStock())) {
            boolean inStock = StringUtils.equals(dto.getStock(), "1");
            boolQueryBuilder.filter(QueryBuilders.termQuery(IN_STOCK, inStock));
        }
        // 5 价格区间
        if (StringUtils.isNotBlank(dto.getPrice())) {
            final String[] prices = dto.getPrice().split("_");
            final Double price1 = prices.length > 0 && NumberUtils.isNumber(prices[0]) ? Double.parseDouble(prices[0]) : null;
            final Double price2 = prices.length > 1 && NumberUtils.isNumber(prices[1]) ? Double.parseDouble(prices[1]) : null;
            final RangeQueryBuilder rangeQuery = QueryBuilders.rangeQuery(PRICE);
            if (price1 != null) {
                rangeQuery.gte(price1);
            }
            if (price2 != null) {
                rangeQuery.lte(price2);
            }
            boolQueryBuilder.filter(rangeQuery);
        }
        // 6 规则参数：attrId11_value11||value12^attrId21_value21||value22
        if (StringUtils.isNotBlank(dto.getAttrs())) {
            final String[] attrs = dto.getAttrs().split("\\^");
            for (String attr : attrs) {
                final String[] ss = attr.split("\\_");
                if (ss.length < 2) {
                    continue;
                }
                final String attrId = ss[0];
                final String[] attrValues = ss[1].split("\\|\\|");
                // 构建 nested 内的 bool 查询
                final BoolQueryBuilder attrsBoolQueryBuilder = QueryBuilders.boolQuery();
                attrsBoolQueryBuilder.must(QueryBuilders.termQuery(BASE_ATTRS_ID, attrId));
                attrsBoolQueryBuilder.must(QueryBuilders.termsQuery(BASE_ATTRS_VALUE, attrValues));
                // 构建 nested 查询
                final NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery(BASE_ATTRS, attrsBoolQueryBuilder, ScoreMode.None);
                boolQueryBuilder.filter(nestedQueryBuilder);
            }
        }
        return boolQueryBuilder;
    }

    @Override
    public SkuDetailsVO details(Long skuId) throws IOException {
        // 1. 根据 skuId 查询 sku 详情
        final SearchResponse skuDetailsResponse = selectSkuDetails(skuId);
        // 2. 如果未命中则直接返回
        if (skuDetailsResponse.getHits().getTotalHits().value < 1) {
            return null;
        }
        // 3. 抽取 sku 详情信息
        final SkuDetailsVO skuDetails = extractSkuDetailsInfo(skuDetailsResponse);
        if (skuDetails == null || StringUtils.isBlank(skuDetails.getSpuId())) {
            return null;
        }
        // 4. 查询 spu 的销售属性
        final SearchResponse spuSaleAttrsResponse = selectSpuSaleAttrs(skuDetails);
        // 5. 如果未命中则直接返回
        if (spuSaleAttrsResponse.getHits().getTotalHits().value < 1) {
            return null;
        }
        // 6. 抽取 spu 的销售信息
        final List<SkuDetailsVO.SaleAttr> saleAttrs = extractSkuSaleAttrs(spuSaleAttrsResponse);
        skuDetails.setSaleAttrs(saleAttrs);
        return skuDetails;
    }

    /**
     * 功能描述：根据 skuId 查询 sku 详情
     *
     * @param skuId skuId
     * @return sku 详情
     * @throws IOException
     */
    private SearchResponse selectSkuDetails(Long skuId) throws IOException {
        // 组装查询条件
        final SearchSourceBuilder builder = new SearchSourceBuilder();
        builder.query(QueryBuilders.termQuery(ID, skuId));
        builder.fetchSource(null, new String[]{CATEGORY, SALE_ATTRS});
        // 执行查询
        final SearchRequest searchRequest = new SearchRequest(new String[]{SKUS_INDEX}, builder);
        return esRestClient.search(searchRequest, OPTIONS);
    }

    /**
     * 功能描述：抽取 sku 详情
     *
     * @param response 响应
     * @return 详情
     * @throws IOException
     */
    private SkuDetailsVO extractSkuDetailsInfo(SearchResponse response) throws IOException {
        final SkuDetailsVO result = new SkuDetailsVO();
        // 解析查询结果
        final SearchHit hit = response.getHits().getAt(0);
        final Map<String, Object> sourceAsMap = hit.getSourceAsMap();
        // 基本信息
        result.setId(MapUtils.getString(sourceAsMap, ID));
        result.setTitle(MapUtils.getString(sourceAsMap, TITLE));
        result.setSubtitle(MapUtils.getString(sourceAsMap, SUBTITLE));
        result.setPrice(MapUtils.getDouble(sourceAsMap, PRICE));
        result.setInStock(MapUtils.getBoolean(sourceAsMap, IN_STOCK));
        // 图片集
        if (sourceAsMap.containsKey(IMAGES)) {
            final List<SkuDetailsVO.Image> images = ((List<Map<String, Object>>) sourceAsMap.get(IMAGES))
                    .stream()
                    .map(image -> {
                        final String url = MapUtils.getString(image, URL);
                        final boolean isDefault = MapUtils.getBoolean(image, DEFAULT);
                        return new SkuDetailsVO.Image(url, isDefault);
                    })
                    .collect(Collectors.toList());
            result.setImages(images);
        }
        // 基础属性
        if (sourceAsMap.containsKey(BASE_ATTRS)) {
            final List<SkuDetailsVO.BaseAttr> baseAttrs = ((List<Map<String, Object>>) sourceAsMap.get(BASE_ATTRS))
                    .stream()
                    .map(baseAttr -> {
                        final String id = MapUtils.getString(baseAttr, ID);
                        final String name = MapUtils.getString(baseAttr, NAME);
                        final String value = MapUtils.getString(baseAttr, VALUE);
                        return new SkuDetailsVO.BaseAttr(id, name, value);
                    })
                    .collect(Collectors.toList());
            result.setBaseAttrs(baseAttrs);
        }
        // 品牌
        if (sourceAsMap.containsKey(BRAND)) {
            final Map<String, Object> brand = MapUtils.getMap(sourceAsMap, BRAND);
            final String id = MapUtils.getString(brand, ID);
            final String name = MapUtils.getString(brand, NAME);
            result.setBrand(new SkuDetailsVO.Brand(id, name));
        }
        // 分组
        if (sourceAsMap.containsKey(BASE_ATTR_GROUPS)) {
            final List<SkuDetailsVO.BaseAttrGroup> baseAttrGroups = ((List<Map<String, Object>>) sourceAsMap.get(BASE_ATTR_GROUPS))
                    .stream()
                    .map(baseAttrGroup -> {
                        final String id = MapUtils.getString(baseAttrGroup, ID);
                        final String name = MapUtils.getString(baseAttrGroup, NAME);
                        final String icon = MapUtils.getString(baseAttrGroup, ICON);
                        final List<String> attrIds = ((List<Long>) baseAttrGroup.get("attrIds")).stream()
                                .map(String::valueOf)
                                .collect(Collectors.toList());
                        return new SkuDetailsVO.BaseAttrGroup(id, name, icon, attrIds);
                    })
                    .collect(Collectors.toList());
            result.setBaseAttrGroups(baseAttrGroups);
        }
        // spu
        final Map<String, Object> spu = MapUtils.getMap(sourceAsMap, SPU);
        result.setSpuId(MapUtils.getString(spu, ID));
        result.setIntroduce(MapUtils.getString(spu, INTRODUCE));
        return result;
    }

    /**
     * 功能描述：查询 spu 的销售属性
     *
     * @param skuDetails sku详情
     * @return 销售属性
     * @throws IOException
     */
    private SearchResponse selectSpuSaleAttrs(SkuDetailsVO skuDetails) throws IOException {
        final SearchSourceBuilder builder = new SearchSourceBuilder();
        // 指定不返回命中数据，只需要聚合结果
        builder.size(0);
        // 查询条件组装
        final BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery(SPU_ID, skuDetails.getSpuId());
        final NestedQueryBuilder nestedQueryBuilder = QueryBuilders.nestedQuery(SPU, termQueryBuilder, ScoreMode.None);
        boolQueryBuilder.filter(nestedQueryBuilder);
        builder.query(boolQueryBuilder);
        // sale_attrs_id_aggs
        final TermsAggregationBuilder saleAttrsIdAggs = AggregationBuilders.terms(SALE_ATTRS_ID_AGGS);
        saleAttrsIdAggs.field(SALE_ATTRS_ID);
        // sale_attrs_name_aggs
        TermsAggregationBuilder saleAttrsNameAggs = AggregationBuilders.terms(SALE_ATTRS_NAME_AGGS);
        saleAttrsNameAggs.field(SALE_ATTRS_NAME);
        saleAttrsIdAggs.subAggregation(saleAttrsNameAggs);
        // sale_attrs_value_aggs
        TermsAggregationBuilder saleAttrsValueAggs = AggregationBuilders.terms(SALE_ATTRS_VALUE_AGGS);
        saleAttrsValueAggs.field(SALE_ATTRS_VALUE);
        saleAttrsNameAggs.subAggregation(saleAttrsValueAggs);
        // sale_attrs_skuid_aggs
        TermsAggregationBuilder saleAttrsSkuidAggs = AggregationBuilders.terms(SALE_ATTRS_SKUID_AGGS);
        saleAttrsSkuidAggs.field(SALE_ATTRS_SKUID);
        saleAttrsValueAggs.subAggregation(saleAttrsSkuidAggs);
        // 嵌套聚合
        final NestedAggregationBuilder nestedAggBuilder = AggregationBuilders.nested(SALE_ATTRS_NESTED_AGGS, SALE_ATTRS);
        nestedAggBuilder.subAggregation(saleAttrsIdAggs);
        builder.aggregation(nestedAggBuilder);
        // 执行查询
        final SearchRequest searchRequest = new SearchRequest(new String[]{SKUS_INDEX}, builder);
        return esRestClient.search(searchRequest, OPTIONS);
    }

    /**
     * 功能描述：抽取响应中的销售属性
     *
     * @param response 响应
     * @return 销售属性
     * @throws IOException
     */
    private List<SkuDetailsVO.SaleAttr> extractSkuSaleAttrs(SearchResponse response) throws IOException {
        final List<SkuDetailsVO.SaleAttr> saleAttrs = new ArrayList<>();
        final Aggregations aggregations = response.getAggregations();
        final ParsedNested saleAttrsNestedAggs = aggregations.get(SALE_ATTRS_NESTED_AGGS);
        if (saleAttrsNestedAggs == null) {
            return null;
        }
        final ParsedLongTerms saleAttrsIdAggs = saleAttrsNestedAggs.getAggregations().get(SALE_ATTRS_ID_AGGS);
        if (saleAttrsIdAggs == null) {
            return null;
        }
        return saleAttrsIdAggs.getBuckets().stream()
                .map(idBucket -> {
                    final SkuDetailsVO.SaleAttr saleAttr = new SkuDetailsVO.SaleAttr();
                    // 解析 attrId
                    saleAttr.setId(idBucket.getKeyAsString());
                    // 解析 attrName
                    final ParsedStringTerms saleAttrsNameAggs = idBucket.getAggregations().get(SALE_ATTRS_NAME_AGGS);
                    saleAttrsNameAggs.getBuckets().forEach(nameBucket -> {
                        saleAttr.setName(nameBucket.getKeyAsString());
                        // 解析 attrValue
                        final ParsedStringTerms saleAttrsValueAggs = nameBucket.getAggregations().get(SALE_ATTRS_VALUE_AGGS);
                        final List<SkuDetailsVO.SaleAttrValue> attrValues = saleAttrsValueAggs.getBuckets().stream()
                                .map(valueBucket -> {
                                    final SkuDetailsVO.SaleAttrValue saleAttrValue = new SkuDetailsVO.SaleAttrValue();
                                    saleAttrValue.setValue(valueBucket.getKeyAsString());
                                    // 解析 skuId
                                    final ParsedLongTerms saleAttrsSkuidAggs = valueBucket.getAggregations().get(SALE_ATTRS_SKUID_AGGS);
                                    final List<String> skuIds = saleAttrsSkuidAggs.getBuckets().stream()
                                            .map(MultiBucketsAggregation.Bucket::getKeyAsString)
                                            .collect(Collectors.toList());
                                    saleAttrValue.setSkuIds(skuIds);
                                    return saleAttrValue;
                                })
                                .collect(Collectors.toList());
                        saleAttr.setValues(attrValues);
                    });
                    return saleAttr;
                })
                .collect(Collectors.toList());
    }

    /**
     * TODO 搜索建议所使用的查询词来源于搜索系统中的用户历史搜索行为日志，因此需要一段时间的业务积累。
     *  用户搜索日志存储在 Hadoop 中。从 Hadoop 中获取用户搜索日志后，需要对搜索关键字进行统计，
     *  如统计每个搜索关键字的搜索次数，统计的数值将作为suggest匹配后的排序权重，最后由初始化程序将处理的结果存入ES中。
     *  参考链接：https://weread.qq.com/web/reader/710325a07279806471049d0k31f320d02bb31fefc0e5450
     *  ----------------------------------------------------------------------------------------
     *  商品的品牌也需要同步进去
     */
    @Override
    public List<String> suggest(SkuSuggestSearchParamsDTO dto) throws IOException {
        // 关键字不能为空
        if (StringUtils.isBlank(dto.getKeyword())) {
            return Collections.emptyList();
        }
        final String keyword = dto.getKeyword();
        // 组装查询条件
        final SearchRequest searchRequest = new SearchRequest(SKUS_SUGGEST_INDEX);
        {
            final SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            final SuggestBuilder suggestBuilder = new SuggestBuilder();
            suggestBuilder.addSuggestion(CHINESE_SUGGESTION_NAME,
                    SuggestBuilders.completionSuggestion(CHINESE).prefix(keyword));
            suggestBuilder.addSuggestion(SPELLING_SUGGESTION_NAME,
                    SuggestBuilders.completionSuggestion(SPELLING).prefix(keyword));
            suggestBuilder.addSuggestion(SIMPLIFIED_SPELLING_SUGGESTION_NAME,
                    SuggestBuilders.completionSuggestion(SIMPLIFIED_SPELLING).prefix(keyword));
            searchSourceBuilder.suggest(suggestBuilder);
            searchRequest.source(searchSourceBuilder);
        }
        // 执行查询
        final SearchResponse response = esRestClient.search(searchRequest, OPTIONS);
        // 提取查询结果
        Suggest suggest = response.getSuggest();
        final List<String> suggestList = new ArrayList<String>();
        suggestList.addAll(extractSuggestChinese(suggest.getSuggestion(CHINESE_SUGGESTION_NAME)));
        suggestList.addAll(extractSuggestChinese(suggest.getSuggestion(SPELLING_SUGGESTION_NAME)));
        suggestList.addAll(extractSuggestChinese(suggest.getSuggestion(SIMPLIFIED_SPELLING_SUGGESTION_NAME)));
        return suggestList;
    }

    /**
     * 功能描述：抽取建议列表
     *
     * @param suggestion CompletionSuggestion
     * @return 搜索建议列表
     */
    private List<String> extractSuggestChinese(CompletionSuggestion suggestion) {
        if (suggestion == null || CollectionUtils.isEmpty(suggestion.getOptions())) {
            return Collections.emptyList();
        }
        final List<String> list = new ArrayList<>();
        for (CompletionSuggestion.Entry.Option option : suggestion.getOptions()) {
            Map<String, Object> sourceMap = option.getHit().getSourceAsMap();
            Map<String, Object> chineseText = (Map<String, Object>) sourceMap.get(CHINESE);                                     //获取文档中的中文
            list.add((String) chineseText.get(INPUT));
        }
        return list;
    }

}
